-module(koordinator).
-export([start/0]).
-import(werkzeug, [shuffle/1, logging/2, timeMilliSecond/0, to_String/1]).
-import(lists, [concat/1]).

%Startet den Koordinator-Prozess und liefert die PID zurück, um manuell Nachrichten hinzuschicken
start() -> 
	{_,CFG} = file:consult("../config/koordinator.cfg"),
	NSNode = proplists:get_value(nameservicenode, CFG),
	NSName = proplists:get_value(nameservicename, CFG),
	KoordName = proplists:get_value(koordinatorname, CFG),
	AutoCorrect = proplists:get_value(korrigieren, CFG), %Das Toggle-Flag
	force_ping(NSNode), %So lange pingen, bis Node da ist
	NSPID = get_ns_pid(NSName),
	spawn(fun() -> run_koordinator(AutoCorrect, NSPID, KoordName) end). %Liefert PID des Prozesses zurück

run_koordinator(AutoCorrect, NSPID, KoordName) ->
	%Vorbereitung
	register(KoordName, self()),
	NSPID ! {self(), {rebind, KoordName, node()}},
	receive ok -> ok end,
	%Programmlauf
	log("Koordinator gestartet"),
	pre_initial(AutoCorrect, NSPID),
	%Aufräumen
	NSPID ! {self(), {unbind, KoordName}},
	receive ok -> ok end,
	unregister(KoordName). 


% ###################################################################################
% Vorbereitungs-Zustände
% ###################################################################################

pre_initial(AutoCorrect, NSPID) ->
	flush_queue(),

	{_, CFG} = file:consult("../config/koordinator.cfg"),
	GGTs = proplists:get_value(ggtprozessnummer, CFG),
	TermZeit = proplists:get_value(termzeit, CFG),
	ArbZeit = proplists:get_value(arbeitszeit, CFG),
	initial(AutoCorrect, NSPID, GGTs, TermZeit, ArbZeit, []).

% Diese Funktion mischt die Nodes und erzeugt anschließend den Ring daraus
pre_bereit(NSPID, AutoCorrect, NSPID, Nodes) ->
	flush_queue(),
	log("Erstelle Ring"),
	Resolved = lookup_nodes(NSPID, shuffle(Nodes)),

	case length(Nodes) < 2 of 
		true -> log("Zu wenig Knoten(unter 2) vorhanden, um einen Ring zu bilden!");
		false -> build_ring(Resolved), log("Ring erstellt."), bereit(NSPID, AutoCorrect, Resolved)
	end.


% ###################################################################################
% Zustände
% ###################################################################################

initial(AutoCorrect, NSPID, GGTs, TermZeit, ArbZeit, Nodes) ->
	receive
		{getsteeringval, PID} -> 
			PID ! {steeringval, ArbZeit, TermZeit, GGTs},
			initial(AutoCorrect, NSPID, GGTs, TermZeit, ArbZeit, Nodes);
		{hello, PName} -> 
			log(concat(["Prozess: ", PName, " hat sich angemeldet"])),
			initial(AutoCorrect,NSPID,GGTs,TermZeit, ArbZeit, [PName | Nodes]);
		toggle -> 
			case AutoCorrect of
				false -> log("Toggle-Flag steht jetzt auf: Automatische Korrektur");
				true  -> log("Toggle-Flag steht jetzt auf: Falsche Terminierung ignorieren")
			end,
			initial(not(AutoCorrect), NSPID, GGTs, TermZeit, ArbZeit, Nodes);
		step -> pre_bereit(NSPID, AutoCorrect, NSPID, Nodes);
		kill -> log("Kill erhalten!")
	end.
bereit(NSPID, AutoCorrect, ResolvedPIDList) ->
	bereit(NSPID, AutoCorrect, ResolvedPIDList, unknown).

bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT) -> 
	receive
		{calc, WunschGGT} -> 
			flush_queue(), 
			initialize_calculation(ResolvedPIDList, WunschGGT), 
			bereit(NSPID, AutoCorrect, ResolvedPIDList, unknown);
		{briefmi, {PName, NewGGT, Time}} when NewGGT < CurrentGGT -> 
			log(concat(["Prozess: ", PName, " hat um ", Time ," einen besseren GGT gefunden: ", to_String(NewGGT)])), 
			bereit(NSPID, AutoCorrect, ResolvedPIDList, NewGGT);
		{briefmi, {PName, NewGGT, Time}} ->
			log(concat(["Prozess: ", PName, " hat um ", Time, " einen GGT gefunden: ",  to_String(NewGGT)])), 
			bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT);
		{pongGGT, PName} -> 
			log(concat(["Prozess: ", PName, " reagiert noch"])),
			bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT);
		{mi, GGT} ->
			log(concat(["Mi: ", GGT])), %%Soll der Ring vielleicht reihum abgefragt werden? Zumal keine Zuordnung zu Prozessen möglich ist!
			bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT);
		{briefterm, {PName, GGT, Time}, Sender} when CurrentGGT < GGT -> %Nicht der Idealwert
			log(concat(["Prozess: ", PName, " meldet um ", Time, " eine Terminierung mit: ", to_String(GGT), " jedoch ist ", to_String(CurrentGGT), " bereits bekannt"])),
			if 
				AutoCorrect -> 
					log("Sende besten bekannten GGT"), 
					Sender ! {sendy, CurrentGGT};
				true -> ok
			end,
			bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT);
		{briefterm, {PName, GGT, Time}, _} -> %Kann jetzt eigentlich nur = dem CurrentGGT sein.
			log(concat(["Prozess: ", PName, " meldet um ", Time, " eine Terminierung mit: ", to_String(GGT)])),
			bereit(NSPID, AutoCorrect, ResolvedPIDList, CurrentGGT);

		prompt -> send_to_all(ResolvedPIDList, {tellmi, self()}), bereit(NSPID, AutoCorrect, ResolvedPIDList);
		nudge -> send_to_all(ResolvedPIDList, {pingGGT, self()}), bereit(NSPID, AutoCorrect, ResolvedPIDList);
		toggle -> 
			case AutoCorrect of
				false -> log("Toggle-Flag steht jetzt auf: Automatische Korrektur");
				true  -> log("Toggle-Flag steht jetzt auf: Falsche Terminierung ignorieren")
			end,
			bereit(NSPID, not(AutoCorrect), ResolvedPIDList, CurrentGGT);
		kill -> 
			log("Kill erhalten! Sende kill an GGT-Prozesse."), 
			send_to_all(ResolvedPIDList, kill);
		reset -> 
			log("Reset erhalten! Sende kill an GGT-Prozesse."),
			send_to_all(ResolvedPIDList, kill),
			pre_initial(AutoCorrect, NSPID)
	end.



% ###################################################################################
% Hilfsfunktionen
% ###################################################################################

% Diese Funktionsgruppe kümmert sich um das korrekte Zusammensetzten des Rings
build_ring([First, Second | Nodes]) ->
	build_ring(First, Second, [First, Second | Nodes]).

build_ring(First, Second, [PreLast, Last]) -> 
	set_neighbors(First, Last, Second),
	set_neighbors(Last, PreLast, First);


build_ring(First, Second, [Left, Node, Right | Nodes]) ->
	set_neighbors(Node, Left, Right),
	build_ring(First, Second, [Node, Right | Nodes]).

% Diese Funktionen verteilen die einzelnen Produkte des GGTs auf
% die einzelnen GGT-Prozesse
distribute_values([{_, PID} | ResolvedPIDList], [GGT | GGTList]) ->
	PID ! {setpm, GGT},
	distribute_values(ResolvedPIDList, GGTList);

distribute_values([], []) ->
	ok.

% Diese Funktion leert die gesamte Message-Queue
flush_queue() -> 
	receive
		_ -> flush_queue()
		after 0 -> ok
	end.

% Probiert so lange einen Knoten anzupingen, bis ein Ping zurückkommt
force_ping(Node) ->
	case net_adm:ping(Node) of
		pang -> force_ping(Node);
		pong -> ok
	end.

% Probiert alle 100ms den Namensdienst aufzulösen, bis es klappt
get_ns_pid(NSName) ->
	timer:sleep(100),
	case global:whereis_name(NSName) of 
		undefined -> get_ns_pid(NSName);
		PID -> PID
	end.

% Verteilt die initialen Y-Werte an die Prozesse und stößt die 
% Berechnung bei 15% der Prozesse an
initialize_calculation(ResolvedPIDList, WunschGGT) -> 
	log(concat(["Starte neue berechnung. WunschGGT=", to_String(WunschGGT), "\n"])),
	distribute_values(ResolvedPIDList, werkzeug:bestimme_mis(WunschGGT,length(ResolvedPIDList))),
	start_calculation(ResolvedPIDList, WunschGGT).


% Loggt die angegebene Zeichenkette in die koordinator.log und beendet mit einem Zeilenumbruch
log(What) ->
	logging("../logs/koordinator.log", concat(["<",timeMilliSecond(),"> ", What,"\n"])).

% Diese Funktionsgruppe fragt für jeden eingetragenen Prozessnamen die PID im Namensdienst 
% nach und löscht ihn aus der Liste, wenn der Namensdienst den Prozessnamen nicht kennt.
lookup_nodes(NSPID, Nodes) ->
	lookup_nodes(NSPID, Nodes, []).

lookup_nodes(_, [], Result) -> 
	Result;

lookup_nodes(NSPID, [PName | Nodes], Result) ->
	NSPID ! {self(), {lookup, PName}},
	receive
		not_found -> log(concat(["Prozess: '", PName, "' ist nicht im Namensdienst eingetragen und wird übersprungen!"]));
		{pin, PID} -> lookup_nodes(NSPID, Nodes, [{PName, PID} | Result]);
		kill -> self() ! kill, Result %Erstmal etwas sinnvolles zurückgeben, dann später aussteigen
	end.

% Wählt 15% der Prozesse aus dem Ring aus(aber mindestens 2) und sendet diesen die Nachrichten
% Dazu wird die Liste der Prozesse so lange um ein Element verkürzt(an zufälliger Stelle), bis
% Nur noch diese 15% übrig sind.
start_calculation(ResolvedPIDList, WunschGGT) ->
	ShortPIDList = select_random_elements(ResolvedPIDList, max(round(length(ResolvedPIDList)*0.15), 2)),
	send_y_to_all(ShortPIDList, WunschGGT).

% Setzt die Nachbarn eines Prozesses
set_neighbors({_, PID}, {Left,_}, {Right,_}) ->
	PID ! {setneighbors, Left, Right}.

% Wählt 'Elemente' zufällig aus der Liste und gibt diese zurück.
select_random_elements(List, Elements) ->
	case length(List) =< Elements of
		true -> List;
		false -> select_random_elements(lists:delete(lists:nth(random:uniform(length(List)),List),List), Elements)
	end.

% Sendet an alle Prozesse in der Liste die gleiche Nachricht
send_to_all([{_,PID} | ResolvedPIDList], Message) ->
	PID ! Message,
	send_to_all(ResolvedPIDList, Message);

send_to_all([], _) ->
	ok.

% Sendet ein y an alle Elemente der Liste entsprechend dem vorgegebenen Wunsch-GGT
send_y_to_all([ {_,PID} | Nodes], [Y | Ys]) ->
	PID ! {sendy, Y},
	send_y_to_all(Nodes, Ys);

send_y_to_all([], []) ->
	ok;

% Diese hier aufrufen!
send_y_to_all(Prozesse, WunschGGT) -> 
	send_y_to_all(Prozesse, werkzeug:bestimme_mis(WunschGGT, length(Prozesse))).